1
|
|
|
'use strict' |
2
|
|
|
|
3
|
|
|
import path from 'path' |
4
|
|
|
import restify from 'restify' |
5
|
|
|
import logger from 'winston' |
6
|
|
|
import * as errors from 'restify-errors' |
7
|
|
|
import chalk from 'chalk' |
8
|
|
|
import config from '../config' |
9
|
|
|
import router from './router' |
10
|
|
|
// import favicon from 'serve-favicon' |
11
|
|
|
import compression from 'compression' |
12
|
|
|
import helmet from 'helmet' |
13
|
|
|
import validator from 'restify-joi-middleware' |
14
|
|
|
import passport from './helpers/passport' |
15
|
|
|
import restifyRender from 'restify-render-middleware' |
16
|
|
|
import { pug } from 'consolidate' |
17
|
|
|
import acceptLanguage from 'accept-language' |
18
|
|
|
import i18n, { languages } from './helpers/i18n' |
19
|
|
|
import socketio from 'socket.io' |
20
|
|
|
import channels from './channels' |
21
|
|
|
import corsMiddleware from 'restify-cors-middleware' |
22
|
|
|
|
23
|
|
|
// set supported languages |
24
|
|
|
acceptLanguage.languages(languages) |
25
|
|
|
|
26
|
|
|
const server = restify.createServer({ |
27
|
|
|
name: 'Restify Devise', |
28
|
|
|
version: '1.0.0', |
29
|
|
|
// https://github.com/restify/node-restify/issues/1219#issuecomment-328499227 |
30
|
|
|
// https://github.com/makeomatic/restify-formatter-jsonapi/issues/2#issuecomment-307285452 |
31
|
|
|
ignoreTrailingSlash: true |
32
|
|
|
}) |
33
|
|
|
|
34
|
|
|
// set websocket app |
35
|
|
|
const io = socketio.listen(server.server) |
36
|
|
|
channels(io) |
37
|
|
|
|
38
|
|
|
server.use(restify.plugins.acceptParser(server.acceptable)) |
39
|
|
|
server.use(restify.plugins.queryParser()) |
40
|
|
|
server.use(restify.plugins.bodyParser()) |
41
|
|
|
server.use(restify.plugins.fullResponse()) |
42
|
|
|
server.use(restify.plugins.gzipResponse()) |
43
|
|
|
|
44
|
|
|
// CORS middleware |
45
|
|
|
const cors = corsMiddleware({ |
46
|
|
|
preflightMaxAge: 5, |
47
|
|
|
allowHeaders: ['Authorization'] |
48
|
|
|
}) |
49
|
|
|
|
50
|
|
|
server.pre(cors.preflight) |
51
|
|
|
server.use(cors.actual) |
52
|
|
|
|
53
|
|
|
// https://www.npmjs.com/package/compression#filter-1 |
54
|
|
|
server.use(compression({ |
55
|
|
|
filter (request, response) { |
56
|
|
|
if (request.headers['x-no-compression']) { |
57
|
|
|
logger.warn('server', 'X-No-Compresseion') |
58
|
|
|
// don't compress responses with this request header |
59
|
|
|
return false |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
// fallback to standard filter function |
63
|
|
|
return compression.filter(request, response) |
64
|
|
|
} |
65
|
|
|
})) |
66
|
|
|
|
67
|
|
|
// https://www.npmjs.com/package/helmet#how-it-works |
68
|
|
|
server.use(helmet()) |
69
|
|
|
server.use(helmet.noCache()) |
70
|
|
|
server.use(helmet.referrerPolicy()) |
71
|
|
|
|
72
|
|
|
// joi validation middleware for restify |
73
|
|
|
server.use(validator({ |
74
|
|
|
joiOptions: { |
75
|
|
|
allowUnknown: false |
76
|
|
|
}, |
77
|
|
|
errorTransformer: (validationInput, joiError) => { |
78
|
|
|
const { type, context } = joiError.details[0] |
79
|
|
|
const retError = new errors.BadRequestError() |
80
|
|
|
retError.body.message = { |
81
|
|
|
warn: `'${i18n.t(context.key)}' ${i18n.t(type, context)}`, |
82
|
|
|
context: joiError.details[0].context |
83
|
|
|
} |
84
|
|
|
return retError |
85
|
|
|
} |
86
|
|
|
})) |
87
|
|
|
|
88
|
|
|
// i18n middleware for restify |
89
|
|
|
server.use(i18n.handler()) |
90
|
|
|
|
91
|
|
|
// add res.render() |
92
|
|
|
server.use(restifyRender({ |
93
|
|
|
engine: pug, |
94
|
|
|
dir: path.join(__dirname, 'views') |
95
|
|
|
})) |
96
|
|
|
|
97
|
|
|
server.pre((request, response, next) => { |
98
|
|
|
logger.info('[server]', request.method, request.url) |
99
|
|
|
next() |
100
|
|
|
}) |
101
|
|
|
|
102
|
|
|
server.pre((req, res, next) => { |
103
|
|
|
const headerValue = req.headers['accept-language'] |
104
|
|
|
|
105
|
|
|
if (headerValue) { |
106
|
|
|
i18n.changeLanguage(acceptLanguage.get(headerValue)) |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
next() |
110
|
|
|
}) |
111
|
|
|
|
112
|
|
|
passport.initialize(server) |
113
|
|
|
|
114
|
|
|
// asset routing added |
115
|
|
|
server.get('/assets/*', restify.plugins.serveStatic({ |
116
|
|
|
directory: __dirname |
117
|
|
|
// default: 'style.css' |
118
|
|
|
})) |
119
|
|
|
|
120
|
|
|
// set routes app |
121
|
|
|
router(server) |
122
|
|
|
|
123
|
|
|
// TODO |
124
|
|
|
// http://restify.com/docs/plugins-api/#auditlogger |
125
|
|
|
// https://github.com/trentm/node-bunyan-winston/blob/master/restify-winston.js#L18-L80 |
126
|
|
|
|
127
|
|
|
// Restify servers emit all the events from the node http.Server and has several other events you want to listen on. |
128
|
|
|
// http://nodejs.org/docs/latest/api/http.html#http_class_http_server |
129
|
|
|
|
130
|
|
|
// When a client request is sent for a URL that does not exist, restify will emit this event. |
131
|
|
|
// Note that restify checks for listeners on this event, and if there are none, responds with a default 404 handler. |
132
|
|
|
// It is expected that if you listen for this event, you respond to the client. |
133
|
|
|
|
134
|
|
|
server.on('NotFound', (request, response, error, next) => { |
135
|
|
|
const url = (request.isSecure()) |
136
|
|
|
? 'https' |
137
|
|
|
: 'http' + '://' + request.headers.host + request.url |
138
|
|
|
|
139
|
|
|
logger.warn('[server]', `Route ${chalk.cyan(url)} not found`) |
140
|
|
|
return next(new errors.NotFoundError()) |
141
|
|
|
}) |
142
|
|
|
|
143
|
|
|
// When a client request is sent for a URL that does exist, but you have not registered a route for that HTTP verb, |
144
|
|
|
// restify will emit this event. Note that restify checks for listeners on this event, and if there are none, |
145
|
|
|
// responds with a default 405 handler. It is expected that if you listen for this event, you respond to the client. |
146
|
|
|
// server.on('MethodNotAllowed', (request, response, next) => {}) |
147
|
|
|
|
148
|
|
|
// When a client request is sent for a route that exists, but does not match the version(s) on those routes, |
149
|
|
|
// restify will emit this event. Note that restify checks for listeners on this event, and if there are none, |
150
|
|
|
// responds with a default 400 handler. It is expected that if you listen for this event, you respond to the client. |
151
|
|
|
// server.on('VersionNotAllowed', (request, response, next) => {}) |
152
|
|
|
|
153
|
|
|
// When a client request is sent for a route that exist, but has a content-type mismatch, |
154
|
|
|
// restify will emit this event. Note that restify checks for listeners on this event, and if there are none, |
155
|
|
|
// responds with a default 415 handler. It is expected that if you listen for this event, you respond to the client. |
156
|
|
|
// server.on('UnsupportedMediaType', (request, response, next) => {}) |
157
|
|
|
|
158
|
|
|
// Emitted after a route has finished all the handlers you registered. |
159
|
|
|
// You can use this to write audit logs, etc. The route parameter will be the Route object that ran. |
160
|
|
|
// server.on('after', (request, response, route, error) => {}) |
161
|
|
|
|
162
|
|
|
// Emitted when some handler throws an uncaughtException somewhere in the chain. |
163
|
|
|
// The default behavior is to just call res.send(error), and let the built-ins in restify handle transforming, |
164
|
|
|
// but you can override to whatever you want here. |
165
|
|
|
server.on('uncaughtException', (request, response, route, error) => { |
166
|
|
|
logger.error('[server]', error.stack) |
167
|
|
|
response.send(error) |
168
|
|
|
}) |
169
|
|
|
|
170
|
|
|
// error handler |
171
|
|
|
server.on('error', (error) => { |
172
|
|
|
onError(error) |
173
|
|
|
}) |
174
|
|
|
|
175
|
|
|
const port = normalizePort(config.server.port) |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Normalize a port into a number, string, or false. |
179
|
|
|
*/ |
180
|
|
|
function normalizePort (val) { |
181
|
|
|
const port = parseInt(val, 10) |
182
|
|
|
|
183
|
|
|
if (isNaN(port)) { |
184
|
|
|
// named pipe |
185
|
|
|
return val |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
if (port >= 0) { |
189
|
|
|
// port number |
190
|
|
|
return port |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
return false |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Event listener for HTTP server "error" event. |
198
|
|
|
*/ |
199
|
|
|
function onError (err) { |
200
|
|
|
if (err.syscall !== 'listen') { |
201
|
|
|
throw err |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
const bind = typeof port === 'string' |
205
|
|
|
? 'Pipe ' + port |
206
|
|
|
: 'Port ' + port |
207
|
|
|
|
208
|
|
|
// handle specific listen errors with friendly messages |
209
|
|
|
/* eslint-disable no-unreachable */ |
210
|
|
|
switch (err.code) { |
|
|
|
|
211
|
|
|
case 'EACCES': |
212
|
|
|
err.message = `${bind} requires elevated privileges` |
213
|
|
|
logger.error('[server]', err.message) |
214
|
|
|
break |
215
|
|
|
|
216
|
|
|
// lsof -i tcp:8088 |
217
|
|
|
// kill -9 <PID> |
218
|
|
|
case 'EADDRINUSE': |
219
|
|
|
err.message = `${bind} is already in use` |
220
|
|
|
logger.error('[server]', err.message) |
221
|
|
|
break |
222
|
|
|
} |
223
|
|
|
throw err |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
module.exports = server |
227
|
|
|
|